<?php
/**
 * robinsk.net
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    App
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Wraps application options with persistent storage in a db table
 *
 * @category   LOSC Framework
 * @package    App
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_App_Options
{
    /**
     * Default namespace
     */
    const DEFAULT_NS = 'default';
    
    /**
     * Singleton instance
     *
     * @var Losc_App_Options
     */
    private static $_instance = null;
    
    /**
     * Contains options in their namespace
     *
     * @var array
     */
    private $_namespaces = array(
        self::DEFAULT_NS => array()
    );
    
    /**
     * Contains options that has been fetched
     *
     * @var array
     */
    private $_fetched = array();
    
    /**
     * Singleton: Only self may construct
     *
     */
    private function __construct()
    {
        $this->_autoload();
    }
    
    /**
     * Singleton: Cloning disabled
     *
     */
    private function __clone()
    {
        
    }
    
    /**
     * Returns options instance
     *
     * @return Losc_App_Options
     */
    public static function getInstance()
    {
        if (self::$_instance === null) {
            self::$_instance = new self();
        }
        
        return self::$_instance;
    }
    
    /**
     * Retrieves the given value, or $default if it doesn't exist
     * 
     * This method is for convenience.
     *
     * @param  string $name       name of option to return
     * @param  string $namespace  [optional] option namespace
     * @return mixed
     * @throws Losc_App_Option_NotFoundException
     * @throws Losc_App_Option_NamespaceNotFoundException
     */
    public static function getOption($name, $namespace = self::DEFAULT_NS)
    {
        return self::getInstance()->get($name, $namespace);
    }
    
    /**
     * Returns true if the given namespaces exists
     *
     * @param  string $namespace
     * @return boolean
     */
    public function hasNamespace($namespace)
    {
        return isset($this->_namespaces[$namespace]);
    }
    
    /**
     * Returns the options in the given namespace
     *
     * @param  string $namespace  [optional] option namespace
     * @return array
     * @throws Losc_App_Option_NamespaceNotFoundException
     */
    public function getNamespace($namespace)
    {
        if ($this->hasNamespace($namespace)) {
            return $this->_namespaces[$namespace];
        } else {
            $this->_fetchNamespace($namespace);
        }
        
        return $this->_namespaces[$namespace];
    }
    
    /**
     * Returns true if the object has the given option
     *
     * @param  string $name       name of option to look for
     * @param  string $namespace  [optional] option namespace
     * @return bool
     */
    public function has($name, $namespace = self::DEFAULT_NS)
    {
        return isset($this->_namespaces[$namespace]) &&
               isset($this->_namespaces[$namespace][$name]);
    }
    
    /**
     * Retrieves the given value, or $default if it doesn't exist
     *
     * @param  string $name       option name
     * @param  string $namespace  [optional] option namespace
     * @return mixed
     * @throws Losc_App_Option_NotFoundException
     */
    public function get($name, $namespace = self::DEFAULT_NS)
    {
        if ($this->has($name, $namespace)) {
            return $this->_namespaces[$namespace][$name];
        } elseif (!isset($this->_fetched[$name])) {
            return $this->_fetchOption($name, $namespace);
        } else {
            $msg = array('Option "%s.%s" does not exist.', $namespace, $name);     
            throw new Losc_App_Option_NotFoundException($msg);
        }
    }
    
    /**
     * Retrieves the option namespace
     *
     * @param  string $namespace
     * @return mixed
     */
    public function __get($namespace)
    {
        return $this->getNamespace($namespace);
    }
    
    /**
     * Adds an option and returns the typed value
     *
     * @param  string $name       option name
     * @param  string $value      option value
     * @param  string $type       option type; string, int, float, bool
     * @param  string $namespace  [optional] option namespace
     * @return mixed
     */
    private function _add($name, $value, $type, $namespace = null)
    {
        switch ($type) {
            case 'int':
            case 'integer':
                $value = (int) $value;
                break;
            case 'float':
            case 'double':
            case 'real':
                $value = (float) $value;
                break;
            case 'bool':
            case 'boolean':
                $value = (bool) $value;
                break;
            default:
                $value = (string) $value;
                break;
        }
        
        if (!$namespace) {
            $namespace = self::DEFAULT_NS;
        }
        
        if (!isset($this->_namespaces[$namespace])) {
            $this->_namespaces[$namespace] = array();
        }
        
        return $this->_namespaces[$namespace][$name] = $value;
    }
    
    /**
     * Loads all autoload options from db
     *
     * @return void
     */
    private function _autoload()
    {
        $r = Doctrine_Query::create()
             ->select('namespace, name, value, type')
             ->from('App_Option')
             ->addWhere('autoload = 1')
             ->execute(array(), Doctrine::HYDRATE_ARRAY);
        
        foreach ($r as $option) {
            $this->_add($option['name'],
                        $option['value'],
                        $option['type'],
                        $option['namespace']);
        }
    }
    
    /**
     * Fetches option matching the given $name
     *
     * @param  string $name     name of option to fetch
     * @param  string $namespace  [optional] option namespace
     * @return mixed 
     * @throws Losc_App_Option_NotFoundException
     */
    private function _fetchOption($name, $namespace = self::DEFAULT_NS)
    {
        $this->_fetched[] = $name;
        
        $r = Doctrine_Query::create()
             ->from('App_Option')
             ->select('namespace, name, value, type')
             ->addWhere('namespace = ?')
             ->addWhere('name = ?')
             ->fetchOne(array($namespace, $name), Doctrine::HYDRATE_ARRAY);
        
        if (!$r) {
            $msg = array('Option "%s.%s" was not found.', $namespace, $name);
            throw new Losc_App_Option_NotFoundException($msg);
        }
        
        return $this->_add($r['name'], $r['value'], $r['type'], $r['namespace']);
    }
    
    /**
     * Fetches option matching the given $name
     *
     * @param  string $namespace
     * @return array
     * @throws Losc_App_Option_NotFoundException
     */
    private function _fetchNamespace($namespace)
    {
        $this->_fetched[] = $name;
        
        $r = Doctrine_Query::create()
             ->from('App_Option')
             ->select('namespace, name, value, type')
             ->addWhere('namespace = ?')
             ->execute(array($namespace), Doctrine::HYDRATE_ARRAY);
        
        foreach ($r as $o) {
            $this->_add($o['name'], $o['value'], $o['type'], $o['namespace']);
        }
        
        return $this->_namespaces[$namespace];
    }
}
